/**
 * Copyright (C) 2011 Taobao Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.taobao.jaket;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import com.google.gson.Gson;

/**
 * json building helper class, eg:
 * <p>
 * Here is an example of how {@link JsonRenderer} is used:
 * 
 * <pre>
 * JsonRenderer renderer = new JsonRenderer();
 * renderer.name(&quot;id&quot;).value(34780);
 * renderer.name(&quot;name&quot;).value(&quot;Xueqiang Mi&quot;);
 * renderer.name(&quot;age&quot;).value(25);
 * String json = renderer.build();
 * </pre>
 * 
 * then, you will get a string in json style like this:
 * 
 * <pre>
 * {"id":34780,"name":"Xueqiang Mi","age":25}
 * </pre>
 * 
 * You can also use a more fluent code style to do the same work:
 * 
 * <pre>
 * String json = new JsonRenderer().name(&quot;id&quot;).value(34780).name(&quot;name&quot;).value(&quot;Xueqiang Mi&quot;).name(&quot;age&quot;).value(25)
 *         .build();
 * </pre>
 * 
 * see more test cases in test packages.
 * 
 * </p>
 * 
 * @author Xueqiang Mi
 */
public final class JsonRenderer {
    private static AtomicInteger $id = new AtomicInteger(0);
    private static Map<Object, String> refCache = new HashMap<Object, String>();

    private StringBuilder builder;
    private Anchors anchors;
    private int position = 0;
    private String parent = "";
    private String key = "";

    public JsonRenderer() {
        this.builder = new StringBuilder();
        this.anchors = new Anchors();
    }

    /**
     * re-initialize the cache and build the result
     */
    public String build() {
        $id = new AtomicInteger(0);
        refCache = new HashMap<Object, String>();
        return builder.toString();
    }

    /**
     * find the position and insert the key string {@code "name":}
     */
    public JsonRenderer name(String name) {
        if (name == null) {
            throw new NullPointerException("name == null");
        }

        parent = key;
        key += name + "$";

        if (anchors.containsKey(key)) {
            // revise the position to insert
            position = anchors.get(key).getTail();
            return this;
        }

        // do insert on the tail
        if (position == 0 || ':' == builder.charAt(position - 1)) {
            String content = "{}";
            builder.insert(position, content);
            anchors.update(position, content.length());
            anchors.put(parent, position, position + content.length() - 1);
            position += content.length() - 1; // the offset to do write
        }

        StringBuilder sb = new StringBuilder();
        if ('{' != builder.charAt(position - 1)) {
            sb.append(",");
        }
        sb.append("\"").append(name).append("\":");

        builder.insert(position, sb.toString());
        anchors.update(position, sb.length());
        position += sb.length();
        return this;
    }

    /**
     * A special reference situation, pointing to itself, eg:
     * 
     * <p>
     * 
     * <pre>
     * JsonRenderer renderer = new JsonRenderer();
     * String json = renderer.name(&quot;self&quot;).refSelf().build();
     * </pre>
     * 
     * will generate json string like this:
     * 
     * <pre>
     * {"self":null,"$ref":0,"$id":0}
     * </pre>
     * 
     * </p>
     */
    public JsonRenderer refSelf() {
        int id = $id.getAndIncrement();
        String refString = "null,\"$ref\":" + id + ",\"$id\":" + id;
        builder.insert(position, refString);
        anchors.update(position, refString.length());
        position += refString.length();
        return this;
    }

    /**
     * process a object without cycle reference
     */
    public JsonRenderer value(Object value) {
        String json = new Gson().toJson(value);
        builder.insert(position, json);
        anchors.update(position, json.length());

        parent = "";
        key = "";
        position = builder.length() - 1;
        return this;
    }

    /**
     * process a object with cycle reference
     */
    public JsonRenderer valueWithRef(Object value) {
        if (refCache.containsKey(value)) {
            String nullString = "null";
            builder.insert(position, nullString);
            anchors.update(position, nullString.length());
            position += nullString.length();

            String refString = ",\"$ref\":" + refCache.get(value);
            builder.insert(position, refString);
            anchors.update(position, refString.length());
            position += refString.length();
            return this;
        } else {
            // put into cache
            int id = $id.getAndIncrement();
            refCache.put(value, String.valueOf(id));
        }

        Field[] fields = value.getClass().getDeclaredFields();
        JsonRenderer renderer = new JsonRenderer();
        try {
            for (Field field : fields) {
                field.setAccessible(true);
                Object object = field.get(value);
                if (JsonPrimitive.isPrimitiveOrString(object)) {
                    renderer.name(field.getName()).value(object);
                } else {
                    renderer.name(field.getName()).valueWithRef(object);
                }
            }
            // add $id property if the object is set into cache
            if (refCache.containsKey(value)) {
                renderer.name("$id").value(refCache.get(value));
            }

            String content = renderer.buildWithoutClear();
            builder.insert(position, content);
            anchors.update(position, content.length());
            position += content.length();

        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        return this;
    }

    private String buildWithoutClear() {
        return builder.toString();
    }
}
